#!/usr/bin/python

"""A small http server to provide an UI for the IR arduino."""

import BaseHTTPServer
import pickle
import json
import os
import select
import struct
import sys
import threading
import time


class ServerContext(object):
  serial_port = '/dev/ttyACM0'
  code_library = None
  code_fetcher = None
  code_sender = None


class CodeLibrary(object):
  def __init__(self):
    try:
      unpickled = pickle.load(file('ir-codes.pickle'))
      self.ir_codes = unpickled['ir_codes']
      self.macros = unpickled['macros']
    except:
      self.ir_codes = {}
      self.macros = {}
    self.last_unknown = 0

  def AppendFromRaw(self, line):
    header_fmt = '!HBBBI'
    header_size = struct.calcsize(header_fmt)
    if len(line) < header_size:
      return line
    header, raw_data_len, decoded_type, num_bits, decoded_value = struct.unpack(
        header_fmt, line[:header_size])
    if header != 0xfedc:
      print 'Wrong header %x', header
      return ''
    if len(line) < header_size + raw_data_len:
      return line
    raw_data = struct.unpack(str(raw_data_len) + 's',
                             line[header_size : header_size + raw_data_len])[0]
    if decoded_value not in [x['decoded_value']
                             for x in self.ir_codes.itervalues()]:
      self.ir_codes['UNKNOWN_' + str(self.last_unknown)] = {
          'decoded_type': decoded_type,
          'num_bits': num_bits,
          'decoded_value': decoded_value,
          'raw_data': raw_data,
      }
      self.last_unknown += 1
      self.LogCodes()
    else:
      print 'Already learned ', decoded_value
    print 'Current ', len(line), header_size, raw_data_len, len(line[header_size + raw_data_len:])
    return line[header_size + raw_data_len: ]

  def Dump(self, wf):
    p = {'ir_codes': self.ir_codes,
         'macros': self.macros}
    wf.write(pickle.dumps(p))

  def DumpNames(self, wf):
    wf.write(json.dumps(sorted(self.ir_codes.keys() + self.macros.keys())))

  def Delete(self, name):
    if name in self.macros:
      del self.macros[name]
    if name in self.ir_codes:
      del self.ir_codes[name]

  def LogCodes(self):
    for k, v in self.ir_codes.iteritems():
      print k
      for kk, vv in v.iteritems():
        if kk == 'raw_data':
          print kk
          print ' '.join([hex(ord(x)) for x in vv])
        else:
          print kk, vv
      print '-' * 15

  def Rename(self, src, dest):
    if src in self.macros:
      target = self.macros
    else:
      target = self.ir_codes
    original = target[src]
    del target[src]
    target[dest] = original

  def SetCode(self, code_name, code_type, code_value, code_numbits):
    ServerContext.code_library.ir_codes[code_name] = {
      'decoded_type': code_type,
      'num_bits': code_numbits,
      'decoded_value': code_value,
      'raw_data': '',
    }

  def SetMacro(self, macro_name, macro_commands):
    for c in macro_commands:
      if c not in self.ir_codes:
        print 'Invalid command ', c
        return
    self.macros[macro_name] = macro_commands

  def _SendCodeOverSerial(self, command):
    print 'Sending ', command
    code = ServerContext.code_library.ir_codes[command]
    print 'Code ', code
    data = struct.pack('!HBBBI' + str(len(code['raw_data'])) + 's',
                       0xfedc,
                       len(code['raw_data']),
                       code['decoded_type'],
                       code['num_bits'],
                       code['decoded_value'],
                       code['raw_data'])
    print 'Raw ', ' '.join([hex(ord(x)) for x in data])
    with file(ServerContext.serial_port, 'wb') as f:
      f.write(data)
      f.flush()

  def Send(self, path):
    command = path.split('/')[-1]
    if command not in self.macros:
      self._SendCodeOverSerial(command)
      return
    for code in self.macros[command]:
      self._SendCodeOverSerial(code)
      time.sleep(0.5)


class CodeFetcher(threading.Thread):
  def __init__(self, code_library):
    threading.Thread.__init__(self)
    self.serial = file(ServerContext.serial_port, 'rb')
    self.code_library = code_library
    self.interrupted = False
    self.pipe = os.pipe()
    self.learning_mode = False

  def run(self):
    line = ''
    while not self.interrupted:
      f = select.select([self.serial, self.pipe[0]], [], [])
      time.sleep(1)
      if self.pipe[0] in f[0]:
        print 'Interrupted'
        break
      line += self.serial.read()
      print '>> serial incoming'
      print 'READ ', len(line)
      if self.learning_mode:
        while True:
          # Try to consume as much as possiible.
          new_line = self.code_library.AppendFromRaw(line)
          if new_line == line:
            break
          line = new_line
      else:
        print ' '.join([hex(ord(x)) for x in line])
        print ''.join([x if ord(x) > 32 and ord(x) < 127 else ' '
                       for x in line])
        line = ''
      print '<< serial incoming'

  def stop(self):
    self.interrupted = True
    os.write(self.pipe[1], 'x')



class HTTPHandler(BaseHTTPServer.BaseHTTPRequestHandler):
  def _Dump(self):
    ServerContext.code_library.Dump(self.wfile)

  def _DumpAll(self):
    with file('ir-codes.pickle', 'w') as w:
      ServerContext.code_library.Dump(w)

  def _DumpNames(self):
    ServerContext.code_fetcher.learning_mode = False
    ServerContext.code_library.DumpNames(self.wfile)

  def _Send(self):
    ServerContext.code_fetcher.learning_mode = False
    ServerContext.code_library.Send(self.path)

  def _Learn(self):
    self.wfile.write('Entering learning mode')
    ServerContext.code_fetcher.learning_mode = True

  def _Delete(self):
    items = self.path.split('/')
    if len(items) == 3:
      name = items[2]
      ServerContext.code_library.Delete(name)

  def _Rename(self):
    items = self.path.split('/')
    if len(items) == 4:
      src = items[2]
      dest = items[3]
      if dest:
        ServerContext.code_library.Rename(src, dest)
      else:
        ServerContext.code_library.Delete(src)

  def _SetCode(self):
    items = self.path.split('/')
    if len(items) >= 5:
      code_name = items[2]
      code_type = int(items[3])
      code_value = int(items[4], 16 if items[4].startswith('0x') else 10)
      code_numbits = int(items[5])
      ServerContext.code_library.SetCode(code_name, code_type, code_value,
                                         code_numbits)

  def _SetMacro(self):
    items = self.path.split('/')
    if len(items) >= 3:
      macro_name = items[2]
      macro_commands = items[3:]
      ServerContext.code_library.SetMacro(macro_name, macro_commands)

  def _SendUI(self):
    items = self.path.split('/')
    if len(items) == 3:
      self.wfile.write(file(items[2], 'r').read())

  def do_GET(self):
    print 'do_GET', self.path
    if self.path == '/d':
      self._Dump()
    elif self.path == '/dall':
      self._DumpAll()
    elif self.path == '/dnames':
      self.send_response(200)
      self.send_header('Content-Type', 'application/json')
      self.end_headers()
      self._DumpNames()
    elif self.path.startswith('/send'):
      self._Send()
    elif self.path == '/learn':
      self._Learn()
    elif self.path.startswith('/del'):
      self._Delete()
    elif self.path.startswith('/rename'):
      self._Rename()
    elif self.path.startswith('/setcode'):
      self._SetCode()
    elif self.path.startswith('/setmacro'):
      self._SetMacro()
    elif self.path.startswith('/ui'):
      self._SendUI()


def main(argv):
  ServerContext.serial_port = sys.argv[1]
  ServerContext.code_library = CodeLibrary()
  ServerContext.code_fetcher = CodeFetcher(ServerContext.code_library)
  try:
    ServerContext.code_fetcher.start()
    server = BaseHTTPServer.HTTPServer(('', 9001), HTTPHandler)
    server.serve_forever()
  except:
    print 'Stopping...'
    ServerContext.code_fetcher.stop()
    server.socket.close()
    print 'Stopped.'


if __name__ == '__main__':
  sys.exit(main(sys.argv))
